<?php

declare(strict_types=1);

/**
 * @Author:     Frank
 * @contact     frank_hhm@163.com
 * @Date:       2023-02-07 16:04:24
 * @Last Modified time: 2023-02-16 15:24:39
 */

namespace addon\wechat\services;

use think\facade\Log;
use think\facade\Cache;
use frank\exception\CommonException;
use frank\utils\helper\StringHelper;
use app\common\services\pay\RefundService;
use app\common\enum\store\order\SourceEnum;
use app\common\enum\pay\PayTypeEnum;
use addon\wechat\enum\RefundEnum;

/**
 * 微信支付
 * Class WxPay
 * @package addon\wechat\services
 */
class WechatPayService
{

    // 微信支付配置
    private $config;

    //商户私钥
    private $privateKey;

    //主机头
    private $httpHeader;

    //请求类型
    private $method = 'GET';

    //api接口
    private $panel = '';

    //加密方式
    private $signAlg = 'sha256WithRSAEncryption';

    private $schema = 'WECHATPAY2-SHA256-RSA2048';

    //api地址
    private $rootUrl = 'https://api.mch.weixin.qq.com';

    //随机字符串
    public $nonce = '';

    //时间戳
    public $stamp = 0;

    //报文主体
    public $body = '';

    //签名
    public $signature = '';

    //平台证书序列号
    private $platSerialNo;

    //
    private $platPublicKey;

    
    /**
     * 构造函数
     * WxPay constructor.
     * @param $config
     */

    // 订单支付模型
    private $payModelClass = [
        SourceEnum::MASTER => 'app\common\services\store\order\PaySuccessService'
    ];

    // 订单退款模型
    private $refundModelClass = [
        SourceEnum::MASTER => 'app\common\services\store\order\RefundSuccessService'
    ];

    public function __construct()
    {
    	$arr = [
    		'wechat_weapp_appid'=>'微信小程序AppId',
    		'wechatpay_mchid'=>'微信支付商户号',
            'wechatpay_apikey'=>'微信支付密钥 ',
            'wechatpay_serial_no'=>'微信支付证书序列号',
    		'wechatpay_apiclient_cert'=>'微信支付证书Cert	',
    		'wechatpay_apiclient_key'=>'微信支付证书Key',
    	];
    	foreach ($arr as $key => $item) {
    		if(empty(sysconf($key))){
    			throw new CommonException($item.'未配置');
    			continue;
    		}
    		$this->config[$key] = sysconf($key);
    	}
        $this->getPrivateKey();
        // $this->getCertificates();
    }


    /**
     * 统一下单API
     */
    public function create($orderNo, $openid, $totalFee,$orderSource = SourceEnum::MASTER,$description = '商品')
    {
        $params = [
            'appid'=>$this->config['wechat_weapp_appid'],
            'mchid' => $this->config['wechatpay_mchid'],
            'description' => $description,
            'out_trade_no' => $orderNo,
            'notify_url' => baseUrl() . '/wechat/api/notify/index',  // 异步通知地址
            'amount'=>[
                'total'=>$totalFee * 100, // 价格:单位分
            ],
            'payer'=>[
                'openid'=>$openid,
            ],
            'attach'=>json_encode(['order_source' => $orderSource]),
        ];
        $data = json_encode($params, JSON_UNESCAPED_UNICODE);
        $this->method = 'POST';
        $this->panel = '/v3/pay/transactions/jsapi';
        $this->body = $data;
        $this->buildHeader();
        $url = $this->rootUrl . $this->panel;
        $result = json_decode($this->post($url, $data), true);
        if(!empty($result['code'])){
            throw new CommonException($result['message']??'失败');
        }
        $this->body = 'prepay_id='.$result['prepay_id'];

        $this->buildSignature();
        $data = [
            'timeStamp' => (string)$this->stamp,
            'nonceStr' => $this->nonce,
            'prepay_id' => $result['prepay_id'],
            'paySign' => $this->signature,
        ];
        return $data;
    }

    /**
     * 关闭订单
     */ 
    public function close($orderNo){
        $params = [
            'mchid' => $this->config['wechatpay_mchid']
        ];
        $data = json_encode($params, JSON_UNESCAPED_UNICODE);
        $this->method = 'POST';
        $this->panel = '/v3/pay/transactions/out-trade-no/'.$orderNo.'/close';
        $this->body = $data;
        $this->buildHeader();
        $url = $this->rootUrl . $this->panel;
        $result = json_decode($this->post($url, $data), true);
        if(!empty($result['code'])){
            throw new CommonException($result['message']??'失败');
        }
        return true;
    }

    // 支付成功回调
    public function notify(){
        $inBody = file_get_contents('php://input');
        if (empty($inBody)) {
            return $this->returnCode(false, 'Not found DATA');
        }
        // 转换通知的JSON文本消息为PHP Array数组
        $inBodyArray = (array)json_decode($inBody, true);
        // 使用PHP7的数据解构语法，从Array中解构并赋值变量
        ['resource' => [
            'ciphertext'      => $ciphertext,
            'nonce'           => $nonce,
            'associated_data' => $aad
        ]] = $inBodyArray;
        // 加密文本消息解密
        $inBodyResource = $this->decrypt($ciphertext, $aad, $nonce);
        _trace('wechatpay',$inBodyResource,'info');
        if (!$inBodyResource) {
            return $this->returnCode(false, '签名错误');
        }
        // 把解密后的文本转换为PHP Array数组
        $payData = (array)json_decode($inBodyResource, true);
        $attach = (array)json_decode($payData['attach'],true);
        if(empty($attach['order_source']) && empty($this->payModelClass[$attach['order_source']])){
            return $this->returnCode(false, '订单来源不明确');
        }
        $service = $this->payModelClass[$attach['order_source']]::instance($payData['out_trade_no']);
        if($payData['trade_state'] !== 'SUCCESS'){
            return $this->returnCode(false, '订单支付不成功');
        }
        // 订单支付成功业务处理
        $service->onPaySuccess(PayTypeEnum::WECHAT,[
            'transaction_id'=>$payData['transaction_id'],
            'pay_price'=>StringHelper::number2($payData['amount']['total'] / 100) //支付金额
        ]);
        return $this->returnCode(true, '成功');
    }


    /**
     * 申请退款
     */
    public function refund($data){
         $params = [
            'out_trade_no' => $data['out_trade_no'],
            'out_refund_no' => $data['out_refund_no'],
            'amount' => [
                'refund'=>$data['refund_price']* 100,
                'total'=>$data['pay_price']* 100,
                'currency'=>'CNY'
            ],
            'notify_url' => baseUrl() . '/wechat/api/notify/refund',  // 异步通知地址
        ];
         $data = json_encode($params, JSON_UNESCAPED_UNICODE);
        $this->method = 'POST';
        $this->panel = '/v3/refund/domestic/refunds';
        $this->body = $data;
        $this->buildHeader();
        $url = $this->rootUrl . $this->panel;
        $result = json_decode($this->post($url, $data), true);
        if(!empty($result['code'])){
            throw new CommonException($result['message']??'失败');
        }
        return true;
    }

    // 退款成功回传
    public function refundNotify(){
        if (!$inBody = file_get_contents('php://input')) {
            $this->returnCode(false, 'Not found DATA');
        }
        _trace('wechatpayrefund',$inBody,'info');
        $inBodyArray = (array)json_decode($inBody, true);
        // 使用PHP7的数据解构语法，从Array中解构并赋值变量
        ['resource' => [
            'ciphertext'      => $ciphertext,
            'nonce'           => $nonce,
            'associated_data' => $aad
        ]] = $inBodyArray;
        // 加密文本消息解密
        $inBodyResource = $this->decrypt($ciphertext, $aad, $nonce);
        if (!$inBodyResource) {
            return $this->returnCode(false, '签名错误');
        }
        // 把解密后的文本转换为PHP Array数组
        $refundData = (array)json_decode($inBodyResource, true);
        $payRefundService = RefundService::instance();
        $refundDetail = $payRefundService->getDetail(['out_refund_no'=>$refundData['out_refund_no']]);

        $status = RefundEnum::data($refundData['refund_status']);

        $payRefundService->update($refundDetail['id'],[
            'payer_total'=>StringHelper::number2($refundData['amount']['payer_total']),
            'payer_refund'=>StringHelper::number2($refundData['amount']['payer_refund']),
            'refund_time'=>_strtotime($refundData['success_time']),
            'refund_transaction'=>$refundData['refund_id'],
            'user_account'=>$refundData['user_received_account'],
            'refund_status'=> $status['_value']
        ]);

        if($refundData['refund_status'] !== 'SUCCESS'){
            return $this->returnCode(false, '订单退款不成功');
        }

        $service = $this->refundModelClass[$refundDetail['source']]::instance($refundData['out_refund_no']);
        
        $service->onRefundSuccess($refundDetail['pay_type'],[
            'refund_price'=>$refundData['amount']['refund']
        ]);
        return $this->returnCode(true, '成功');
    }

    /**
     * 返回状态给微信服务器
     * @param boolean $returnCode
     * @param string $msg
     */
    private function returnCode($returnCode = true, $msg = null)
    {
        // 返回状态
        $return = [
            'code' => $returnCode ? 'SUCCESS' : 'FAIL',
            'message' => $msg ?: '成功',
        ];
        return json($return);
        die($this->toXml($return));
    }

    /**
     * 生成Header
     */
    private function buildHeader($contentLength = true)
    {
        $this->httpHeader = array(
            'Accept: application/json',
            'User-Agent:' . $this->config['wechatpay_mchid'],
            'Content-Type: application/json',
        );
        $this->httpHeader[] = 'Authorization: ' . $this->buildAuthorization();
        if ($this->body && $contentLength) {
            $this->httpHeader[] = 'Content-Length: ' . strlen($this->body);
        }
    }
    /**
     * 生成Authorization
     */
    private function buildAuthorization()
    {
        if (!in_array($this->signAlg, \openssl_get_md_methods(true))) {
            throw new CommonException('当前PHP环境不支持'.$this->signAlg);
        }
        $this->signer();
        $str = 'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"';
        $token = sprintf($str, $this->config['wechatpay_mchid'], $this->nonce, $this->stamp, $this->config['wechatpay_serial_no'], $this->signature);
        return $this->schema . ' ' . $token;
    }
    /**
     * 签名生成器
     */
    private function signer()
    {
        $this->stamp = time();
        $this->nonce = $this->buildNonceStr();

        $url_parts     = parse_url($this->panel);
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        $message = $this->method . "\n" . $canonical_url . "\n" . $this->stamp . "\n" . $this->nonce . "\n" . $this->body . "\n";
        openssl_sign($message, $signature, $this->privateKey, $this->signAlg);
        $this->signature = base64_encode($signature);
    }
    /**
     * 生成JSSDK签名
     */
    private function buildSignature()
    {
        $this->stamp = time();
        $this->nonce = $this->buildNonceStr();
        $message = $this->config['wechat_weapp_appid'] . "\n" . $this->stamp . "\n" . $this->nonce . "\n" . $this->body . "\n";
        openssl_sign($message, $signature, $this->privateKey, $this->signAlg);
        $this->signature = base64_encode($signature);
    }
    /*
     * 生成随机字符串
     */
    private function buildNonceStr($len = 32)
    {
        $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $str = '';
        for ($i = 0; $i < $len; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }
    /**
     * 报文解密
     */
    public function decrypt($text, $assoc, $nonce)
    {
        $check = extension_loaded('sodium');
        if (false === $check) {
            return false;
        }
        $check = sodium_crypto_aead_aes256gcm_is_available();
        if (false === $check) {
            return false;
        }
        return sodium_crypto_aead_aes256gcm_decrypt(base64_decode($text), $assoc, $nonce, $this->config['wechatpay_apikey']);
    }
    /*
    加密
    */
    public function getEncrypt($str){
        $encrypted = '';
        if(openssl_public_encrypt($str, $encrypted,$this->platPublicKey, OPENSSL_PKCS1_OAEP_PADDING)){
            $sign = base64_encode($encrypted);
        }else{
            throw new CommonException('encrypt failed');
        }
        return $sign;
    }

    /**
     * 获取商户私钥
     */
    private function getPrivateKey()
    {
        $this->privateKey = openssl_pkey_get_private($this->config['wechatpay_apiclient_key']);
    }
    
    /*
     * post请求
     */
    private function post($url, $data = '')
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->httpHeader);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }
    /*
     * get请求
     */
    private function get($url)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->httpHeader);
        $out = curl_exec($ch);
        curl_close($ch);
        return $out;
    }
}